package dproxy.server.net.access;

import java.io.IOException;
import java.net.Socket;
import java.net.SocketException;

import jplog.collector.JPLog;
import jplog.collector.JPLogFactory;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import dproxy.server.cache.bo.CacheBO;
import dproxy.server.cache.to.RequestResponse;
import dproxy.server.common.exceptions.AppInfrastructureException;
import dproxy.server.common.exceptions.PerformRemoteCallException;
import dproxy.server.common.exceptions.ResponseReadException;
import dproxy.server.common.util.UtilConvert;
import dproxy.server.common.xml.to.IntermediareConfig;
import dproxy.server.net.protocol.Protocol;

/**
 * Process that receives user requests and forwards
 * to remote servers.
 */
public class Access implements Runnable {

    /** How many character of the request will be shown to the user. */
    private static final int ERROR_MESSAGE_LOG_LENGHT = 200;
    
    /** Logger. */
    private Log logger = LogFactory.getLog(getClass());
    /** Socket connecting to the user. */
    private Socket userSocket;
    /** Socket connecting to remote server. */
    private Socket remoteSocket;
    /** Configuration of the intermediare that started this access. */
    private IntermediareConfig ic;
    /** Sequencial access number generator. */
    private static int accessSequencialNumber = 0;
    /** Sequencial number of this request. */
    private final int accessNumber;
    
    /**
     * Creates a new Access handler.
     * @param userSocket Socket connecting to the user
     * @param ic Configuration of the intermediare that started this access
     */
    public Access(
            Socket userSocket, 
            IntermediareConfig ic) {
        // Generates the access number:
        synchronized (Access.class) {
            accessSequencialNumber++;
            accessNumber = accessSequencialNumber;
        }
        logger.debug("Starting request [" + accessNumber + "]");
        this.userSocket = userSocket;
        this.ic = ic;
    }

    /**
     * Runnable method.
     */
    public void run() {
        try { 
            service();
        } catch (AppInfrastructureException e) {
            logger.error(
                    "Error performing request : [" + accessNumber + "]" +
                    "Service : " + ic.getProtocol().getName(), e);
        } finally {
            try {
                // Clean up resources
                if (remoteSocket != null) {
                    if (!remoteSocket.isClosed()) {
                        remoteSocket.close();
                    }
                }
                if (userSocket != null) {
                    if (!userSocket.isClosed()) {
                        userSocket.close();
                    }
                }
                logger.debug("Request end");
            } catch (IOException e) {
                logger.error(
                    "Error closing request resources : [" + accessNumber + "]" +
                    "Service : " + ic.getProtocol().getName(), e);
            }
        }
    }

    /**
     * Performs the request/response logic.
     */
    private void service() {
        JPLog jpLog = JPLogFactory.getInstance().newLog("Srvc[" + accessSequencialNumber + "]");
        jpLog.register("Start service()");
        try {
            // Reads a request and generates the key
            byte [] request = ic.getProtocol()
                .readRequest(userSocket.getInputStream());
            jpLog.register("request read");
            if (logger.isDebugEnabled()) {
                logger.debug("Request size [" + request.length + "] bytes");
            }
            
            // Check if this request is cached:
            byte[] response;
            try {
                RequestResponse rr = CacheBO.getInstance(ic.getDatabaseImplentation())
                    .getResponseFromCache(request, ic.getProtocol());
                
                response = rr.getResponseContents();
                
                jpLog.register("cache hit");
                
                
                logger.info("[" + accessNumber + "] Using LOCAL data - DB-ID [" + rr.getIndex() + "]");
            } catch (PerformRemoteCallException e) {
                jpLog.register("cache miss");
                logger.info("[" + accessNumber + "] Using REMOTE data");
                
                // Creates a connection to the remote server and send user request
                remoteSocket 
                    = new Socket(
                            ic.getRemoteHost(), 
                            ic.getRemotePort());
                remoteSocket.setSoTimeout(Protocol.DEFAULT_SO_TIMEOUT);
                remoteSocket.getOutputStream().write(request);
                remoteSocket.getOutputStream().flush();
                
                // Reads response
                try {
                    response = ic.getProtocol()
                        .readResponse(remoteSocket.getInputStream());
                    // Adds response to cache
                    RequestResponse rr = CacheBO.getInstance(ic.getDatabaseImplentation())
                        .add(
                            ic.getProtocol().generateKey(request), 
                            request, 
                            response);
                    String logMessage = "[" + accessNumber + "] Reading REMOTE data : Success";
                    if (rr.getIndex() != null) {
                        logMessage += " - DB-ID [" + rr.getIndex() + "]";
                    }
                    logger.info(logMessage);
                } catch (ResponseReadException rre) {
                    // Server responded with an error
                    logger.warn("[" + accessNumber + "] Server responded with an error : [" 
                            + rre.getErrorDetails() + "]");
                    if (logger.isDebugEnabled()) {
                        logger.debug("Error response contents:");
                        logger.debug(UtilConvert.convertToDisplay(rre.getErrorContents(), ERROR_MESSAGE_LOG_LENGHT));
                    }
                    response = rre.getErrorContents();
                }
                jpLog.register("remote request");
            }
            
            // Send response to user
            if (logger.isDebugEnabled()) {
                logger.debug("Response size [" + response.length + "] bytes");
            }
            userSocket.getOutputStream().write(response);
            userSocket.getOutputStream().flush();
            jpLog.register("response write");
        } catch (SocketException e) {
            throw new AppInfrastructureException(e);
        } catch (IOException e) {
            throw new AppInfrastructureException(e);
        }
        jpLog.submit();
    }
    
}
